/* eslint-disable no-console */
import { Heartbeat } from './Heartbeat';

export type TState = 'connecting' | 'connected' | 'reconnecting' | 'closed';
export interface THeartbeat {
  start: () => void;
  stop: () => void;
  pong: (data: any) => void;
  onRetry: undefined | (() => void);
};

/**
 * WebSocket 关闭状态码
 *
 * @see https://developer.mozilla.org/zh-CN/docs/Web/API/CloseEvent
 */
export enum WSCloseCode {
  /** 正常关闭; 无论为何目的而创建，该链接都已成功完成任务。 */
  CLOSE_NORMAL = 1000,
  /** 稍后重试 */
  RETRY_AFTER = 4500,
  /** 重试失败 */
  RETRY_FAILED = 4501,
  /** 由于新客户端加入，关闭当前客户端连接 */
  NEW_USER_CLIENT = 4502,
}

export class WebSocketClient {
  private ws: undefined | WebSocket;
  private connectPromise: undefined | ReturnType<typeof createPromise<boolean>>;
  private heartbeat: THeartbeat;
  private bindedEvents: Array<() => void> = [];
  private _state: 'connecting' | 'connected' | 'reconnecting' | 'closed' = 'closed';

  private retryCount = 0;
  private retryCountMax = 10;
  private retryInterval = 3000;

  public onMessage: undefined | ((event: MessageEvent) => void);
  public onClose: undefined | ((event: CloseEvent) => void);
  public onError: undefined | ((event: Event) => void);
  public onOpen: undefined | ((event: Event) => void);
  public onConnectCountdown: undefined | ((count: number, countdown: number) => void);
  public onDelayChange: undefined | ((delay: number) => void);
  public onStateChange: undefined | ((state: TState) => void);

  public constructor(private url: string, heartbeat?: THeartbeat) {
    this.heartbeat = heartbeat || new Heartbeat({
      ping: () => this.send('ping'),
      pong: data => data === 'pong',
    });
    this.heartbeat.onRetry = () => {
      console.log('💔心跳失败，重试');
      this.state = 'reconnecting';
      this.close(WSCloseCode.RETRY_AFTER);
    };
  }

  private set state(value: TState) {
    if (this._state === value)
      return;

    this.onStateChange?.(value);
    this._state = value;
  }

  public get state() {
    return this._state;
  }

  public async connect() {
    const canConnect = this.ws?.readyState === WebSocket.CLOSED
      || this.ws?.readyState === WebSocket.CLOSING
      || this.state === 'closed';

    if (!canConnect)
      return;

    this.state = 'connecting';
    this.connectPromise = createPromise();
    this.ws = new WebSocket(this.url);
    this.ws.binaryType = 'arraybuffer';

    this.onWsEvent('open', this._onOpen.bind(this));
    this.onWsEvent('close', this._onClose.bind(this));
    this.onWsEvent('error', this._onError.bind(this));
    this.onWsEvent('message', this._onMessage.bind(this));

    this.onOpen && this.onWsEvent('open', this.onOpen);
    this.onClose && this.onWsEvent('close', this.onClose);
    this.onError && this.onWsEvent('error', this.onError);

    return this.connectPromise.promise;
  }

  public send(data: any) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(data);
      return true;
    }

    return false;
  }

  public close(code?: number, reason?: string) {
    console.log('连接关闭：', code, reason);
    this.ws?.close(code, reason);
  }

  private _onMessage(event: MessageEvent) {
    const data = event.data;
    this.heartbeat.pong(data);
    this.onMessage?.(event);
  }

  private _onOpen() {
    this.connectPromise?.resolve(true);
    this.heartbeat.start();
    this.state = 'connected';
  }

  private _onError() {
    console.log('ws连接发生错误');
    this.connectPromise?.reject({ type: 'error', message: '' });
  }

  private _onClose(event: CloseEvent) {
    this.heartbeat.stop();
    this.connectPromise?.reject({ type: 'close', message: event.reason });
    this.state = 'closed';

    const noNeedRetry = [WSCloseCode.CLOSE_NORMAL, WSCloseCode.RETRY_FAILED, WSCloseCode.NEW_USER_CLIENT].includes(event.code);

    if (noNeedRetry) {
      this.retryCount = 0;
      return;
    }
    console.log('连接关闭OnClose：', event);

    this.retry();
  }

  private onWsEvent(type: keyof WebSocketEventMap, fn: (args: any) => any) {
    this.ws?.addEventListener(type, fn);
    this.bindedEvents.push(() => this.ws?.removeEventListener(type, fn));
  }

  private async retry() {
    if (this.retryCount >= this.retryCountMax) {
      this.retryCount = 0;
      this.heartbeat.stop();
      this.ws?.dispatchEvent(new CloseEvent('close', { code: WSCloseCode.RETRY_FAILED }));
      this.state = 'closed';

      return;
    }

    this.bindedEvents.forEach(unbind => unbind());
    this.state = 'reconnecting';
    this.retryCount++;
    const immediatelyRetry = Math.ceil(this.retryCountMax / 3) <= this.retryCount;

    if (immediatelyRetry) {
      this.connect().then(() => {
        this.retryCount = 0;
      });
    }
    else {
      let retryCountdown = this.retryInterval / 1000;
      const retryCountdownId = setInterval(() => {
        this.onConnectCountdown?.(this.retryCount, retryCountdown);
        retryCountdown--;

        if (retryCountdown <= 0) {
          clearInterval(retryCountdownId);
          this.connect().then(() => {
            this.retryCount = 0;
          });
        }
      }, 1000);
    }
  }
}

function createPromise<T = any>() {
  let resolve = (_value: T) => { };
  let reject = (_reason?: any) => { };

  const promise = new Promise<T>((res, rej) => {
    resolve = res;
    reject = rej;
  });

  return { promise, resolve, reject };
}
